/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2014-2019.
 * Description: a logbuf lib for kbox, like printk's logbuf mechanism.
 *              the code is ported form printk.c of linux kernel source 2.6.21.
 * Author: lichaoyue
 * Create: 2014-3-24
 */

#include <linux/ktime.h>
#include <linux/timekeeping.h>
#include <linux/minmax.h>
#include "kbox_include.h"

#define TS_BUFSIZE 16
/* support only to  x86 and arm architecture */
#ifdef MODULES_VADDR
#define KERNEL_OFFSET (MODULES_VADDR > PAGE_OFFSET ? PAGE_OFFSET : MODULES_VADDR)
#else
#include <asm/page.h>
#define KERNEL_OFFSET (PAGE_OFFSET)
#endif

#ifndef CONFIG_RTOS_KBOX_MINI
bool record_time;
module_param_named(time, record_time, bool, S_IRUSR | S_IWUSR | S_IRGRP);
#endif

/*
 * Write the buffer content of specified length directly to region buffer
 * |head|---body---|head|---body---|A5A5A5A5|--------|
 *  4b         n b          4b         n b         4b
 */


void zero_log_buf(kbox_region_cb *cb, int do_dump, char *warn_str)
{
#ifndef CONFIG_RTOS_KBOX_MINI
	/* there we disable irq, so we start a thread to do dump, and we can't call printk here */
	if (do_dump)
		(void)wakeup_kboxd(cb->log_buf, cb->size, warn_str, cb->name);
#endif
	/* no clear history region info */
	if (cb->is_history == TRUE)
		return;
	set_desc_real_size(cb->pregion_des, 0);
	set_desc_log_start(cb->pregion_des, 0);
	set_desc_log_end(cb->pregion_des, 0);
	set_desc_size(cb->pregion_des, cb->size);
	if (atomic_read(&cb->proc_read_count) != 0)
		cb->reset_fpos = true;
}

static void print_desc_warn(kbox_region_cb *cb, unsigned int msg_len)
{
	char warn_str[KBOX_WARNNING_LEN] = {0};
	snprintf(warn_str, KBOX_WARNNING_LEN, "[%s:%d]size:%u, start:%u, end:%u, region:%s, msg_len=%u\n",
		__func__, __LINE__, get_desc_size(cb->pregion_des),
		get_desc_log_start(cb->pregion_des),
		get_desc_log_end(cb->pregion_des), cb->name, msg_len);
	zero_log_buf(cb, 1, warn_str);
}

static void write_to_ring_buffer(kbox_region_cb *cb, const char *buff, unsigned int len)
{
	unsigned int write_len = len;
	unsigned int *msg_head = NULL;
	unsigned int end_position;
	unsigned int *end_magic = NULL;
	const char *source = buff;
	char *from = NULL;
	u32 log_end = get_desc_log_end(cb->pregion_des);

	from = &(cb->log_buf[log_end % cb->size]);

	/* recode the message len */
	msg_head = (unsigned int *)from;
	from += MSG_HEAD_SIZE;

	if (cb->size - (from - cb->log_buf) < ALIGN(len, MSG_ALIGN)) {
		write_len = cb->size - (from - cb->log_buf);
#ifdef CONFIG_RTOS_PPC_DCBZ_ON_IOADDR_BUGFIX
		generic_memcpy(from, source, write_len);
#else
		memcpy(from, source, write_len);
#endif
		from = cb->log_buf;
		source += write_len;
		write_len = len - write_len;
	}
#ifdef CONFIG_RTOS_PPC_DCBZ_ON_IOADDR_BUGFIX
	generic_memcpy(from, source, write_len);
#else
	memcpy(from, source, write_len);
#endif
	end_position = log_end + (ALIGN(len, MSG_ALIGN) + sizeof(unsigned int));
	end_magic = (unsigned int *)(cb->log_buf + end_position % cb->size);
	*end_magic = KBOX_LOGBUF_END_MAGIC;

	/* after write all data, we update the message len */
	*msg_head = len;
	set_desc_log_end(cb->pregion_des, end_position);
}

static void kbox_write_data(kbox_region_cb *cb, const char *buff, unsigned int len)
{
	unsigned int need_len, free_len;
	unsigned int msg_len;
	u32 log_start = get_desc_log_start(cb->pregion_des);
	u32 log_end = get_desc_log_end(cb->pregion_des);

	need_len = (ALIGN(len, MSG_ALIGN) + MSG_HEAD_SIZE + MSG_END_SIZE);
	free_len = cb->size - (log_end - log_start);

	if (atomic_read(&cb->proc_read_count) != 0) {
		/* |s----f----e   |, if write len > size - (end - fpos), need reset fpos */
		if (need_len > (cb->size + cb->fpos - log_end))
			cb->reset_fpos = true;
	}

	while (free_len < need_len) {
		msg_len =  *(unsigned int *)(cb->log_buf + (log_start % cb->size));
		if (ALIGN(msg_len, MSG_ALIGN) + MSG_HEAD_SIZE + MSG_END_SIZE > cb->size) {
			print_desc_warn(cb, msg_len);
			break;
		} else {
			log_start += (ALIGN(msg_len, MSG_ALIGN) + MSG_HEAD_SIZE);
			set_desc_log_start(cb->pregion_des, log_start);
			if (log_start > log_end) {
				print_desc_warn(cb, msg_len);
				break;
			}
		}
		free_len = cb->size - (log_end - log_start);
	}

	write_to_ring_buffer(cb, buff, len);
}

#ifndef CONFIG_RTOS_KBOX_MINI
static void write_record_time_stamp(kbox_region_cb *cb)
{
	struct timespec64 ts;
	struct rtc_time tm;
	/* one for \0, end of string */
	char timestamp_buf[TS_BUFSIZE + 1] = {"\0"};

	(void)do_gettimeofday_snapshot(&ts);
	rtc_time64_to_tm(ts.tv_sec - (sys_tz.tz_minuteswest * 60), &tm);
	snprintf(timestamp_buf, TS_BUFSIZE + 1, "[%02d%02d%02d%02d%02d%02d]",
		 tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour,
		 tm.tm_min, tm.tm_sec);
	kbox_write_data(cb, timestamp_buf, TS_BUFSIZE);
}
#endif

static void adjust_region_info(kbox_region_cb *cb)
{
	u32 log_start, log_end;

	log_start = get_desc_log_start(cb->pregion_des);
	log_end = get_desc_log_end(cb->pregion_des);
	/* if it runs for a long time, the recorded messages len may be more than a long var */
	if (log_start >= cb->size) {
		/* one message would not long than log_buf_len */
		log_start -= cb->size;
		log_end -= cb->size;
		set_desc_log_start(cb->pregion_des, log_start);
		set_desc_log_end(cb->pregion_des, log_end);
	}

	set_desc_real_size(cb->pregion_des, log_end - log_start);
}

static unsigned int process_region_limit_size(kbox_region_cb *cb, unsigned int size)
{
	unsigned int limit_size = cb->size - MSG_HEAD_SIZE - MSG_END_SIZE;
	unsigned int processed_size = size;

#ifndef CONFIG_RTOS_KBOX_MINI
	if (record_time && !cb->forbidden_record_time)
		limit_size -= (TS_BUFSIZE + MSG_HEAD_SIZE);
#endif
	if (processed_size > limit_size || get_desc_log_start(cb->pregion_des) > cb->size) {
		processed_size = min(limit_size, processed_size);
		set_desc_log_start(cb->pregion_des, 0);
		set_desc_log_end(cb->pregion_des, 0);
		set_desc_real_size(cb->pregion_des, 0);

		if (atomic_read(&cb->proc_read_count) != 0)
			cb->reset_fpos = true;
	}

	return processed_size;
}

static int kbox_do_write(int rfd, const char *buff, unsigned int len)
{
	int ret;
	unsigned long flags;
	kbox_region_cb *cb = NULL;
	unsigned int processed_size = len;

	ret = kbox_get_region(rfd, &cb);
	if (ret != 0) {
		/* not find this region */
		ret = kbox_get_region(KBOX_DEFAULT_REG_ID, &cb);
		if (ret != 0)
			return -EINVAL;
	}

	if (cb->is_history == TRUE) {
		/* history region is readonly */
		kbox_put_region(cb);
		return -EACCES;
	}

	kbox_printk_lock(&cb->logbuf_lock, &flags);
#ifndef CONFIG_RTOS_KBOX_MINI
	if (kbox_control_isstop())
		goto out;
#endif
	processed_size = process_region_limit_size(cb, len);
#ifndef CONFIG_RTOS_KBOX_MINI
	if (record_time && !cb->forbidden_record_time) {
		if (cb->is_newline) {
			cb->is_newline = false;
			write_record_time_stamp(cb);
		}
		if (buff[processed_size - 1] == '\n')
			cb->is_newline = true;
	}
#endif
	kbox_write_data(cb, buff, processed_size);
	adjust_region_info(cb);
#ifndef CONFIG_RTOS_KBOX_MINI
out:
#endif
	kbox_printk_unlock(&cb->logbuf_lock, &flags);
	kbox_put_region(cb);

	return processed_size;
}

int kbox_write(int rfd, const char *buff, unsigned len)
{
	if (kbox_dev_cb.dev_status != KBOX_DEV_STATUS_FS_OK)
		return -EBUSY;

	if (len == 0 || rfd == KBOX_NULL_REGION_ID)
		return 0;

	if (rfd < 0 || buff == NULL)
		return -EINVAL;

#ifdef CONFIG_PPC64
	if (!is_kernel_addr((uintptr_t)buff))
		return -EINVAL;
#else
	if ((uintptr_t)buff < KERNEL_OFFSET)
		return -EINVAL;
#endif
	return kbox_do_write(rfd, buff, len);
}
EXPORT_SYMBOL(kbox_write);

